using System;
using System.Collections.Generic;
using System.IO;
using System.Timers;

namespace Fadd.Globalization.Yaml
{
    /// <summary>
    /// Class to subscribe to changeevents on a file and update a languagehierarchy every time the file changes.
    /// </summary>
    /// <remarks>
    /// If the file being watched is deleted or renamed no more changes will be made to the languagenode.
    /// Also be observant on the fact that the class cannot Remove entries from a LanguageNode, only add or modify entries.
    /// This restriction is needed because a single YamlWatcher only watches One file and a LanguageNode can be connected to several
    /// watchers and files, deleting anything could clear things for another file.
    /// </remarks>
    public class YamlWatcher : IDisposable, ILanguageWatcher
    {
        public static char PathSeparator = '\\';
        private readonly string _path;
        private FileSystemWatcher _watcher;
        private readonly LanguageNode _rootNode;
        private readonly Dictionary<string, FileMapping> _files = new Dictionary<string, FileMapping>();

        /// <summary>
        /// files that need to be reloaded
        /// </summary>
        private readonly Queue<string> _filesToReload = new Queue<string>();

        private class FileMapping
        {
            public readonly LanguageNode Language;
            public readonly string FullPath;
            public FileMapping(LanguageNode node, string path)
            {
                Language = node;
                FullPath = path;
            }
        }

        /// <summary>A timer to use as delay if the file is currently in use</summary>
        private readonly Timer _readTimer = new Timer(5000);

        /// <summary>
        /// Instantiates the class to listen to changes in a file, also reads the file and fills the languageNode with language data
        /// </summary>
        /// <param name="languageNode">LanguageNode to fill</param>
        /// <param name="filename">The filename to watch</param>
        public YamlWatcher(LanguageNode languageNode, string filename)
        {
            if (!File.Exists(filename))
                throw new FileNotFoundException(filename + " do not exist.", filename);

            string fullPath = Path.GetFullPath(filename);
            _path = Path.GetDirectoryName(fullPath);
            _rootNode = languageNode;
            _files.Add(Path.GetFileName(filename), new FileMapping(_rootNode, fullPath));
            LoadFile(filename, _rootNode);
            Watch();

            _readTimer.Elapsed += OnTryRead;
        }

        /// <summary>
        /// Root language node.
        /// </summary>
        /// <value></value>
    	public LanguageNode RootNode
    	{
			get { return _rootNode; }
    	}


        /// <summary>
        /// Load and watch another file on disk.
        /// </summary>
        /// <param name="filename">Relative or absolute path to language file.</param>
        /// <returns>Node that have been loaded.</returns>
        public ILanguageNode Add(string filename)
        {
            string nodeName = Path.GetFileNameWithoutExtension(filename);
            LanguageNode node = _rootNode.AddChild(nodeName);
            LoadFile(filename, node);
            _files.Add(Path.GetFileName(filename), new FileMapping(_rootNode, Path.GetFullPath(filename)));
            return node;
        }

        /// <summary>
        /// Callback for when the file should be read again
        /// </summary>
        void OnTryRead(object sender, ElapsedEventArgs e)
        {
            string filename;
            lock (_filesToReload)
                filename = _filesToReload.Count != 0 ? _filesToReload.Dequeue() : null;

            string failedFile = null;
            while (filename != null && failedFile != filename)
            {
                if (!LoadFile(filename) && failedFile == null)
                    failedFile = filename;

                lock (_filesToReload)
                    filename = _filesToReload.Count != 0 ? _filesToReload.Dequeue() : null;
            }
        }

        /// <summary>
        /// Sets the class to watch the specified file
        /// </summary>
        private void Watch()
        {
            _watcher = new FileSystemWatcher(_path, "*.yaml");
            _watcher.EnableRaisingEvents = true;
            _watcher.Changed += OnFileChanged;
        }

        /// <summary>
        /// Callback for when the file changes
        /// </summary>
        private void OnFileChanged(object sender, FileSystemEventArgs e)
        {
            LoadFile(e.FullPath);
        }

        /// <summary>
        /// Private loading of file to wrap read failure control
        /// </summary>
        private bool LoadFile(string filename)
        {
            try
            {
                FileMapping mapping;
                if (_files.TryGetValue(Path.GetFileName(filename), out mapping))
                    LoadFile(mapping.FullPath, mapping.Language);
                return true;
            }
            catch (IOException)
            {
                lock (_filesToReload)
                    _filesToReload.Enqueue(filename);
                return false;
            }
        }

        /// <summary>
        /// Fill the rootNode with languages and categories from the specified file
        /// </summary>
        /// <param name="fullPath">Full file path</param>
        /// <param name="rootNode">The rootNode to fill</param>
        /// <exception cref="ArgumentException">If rootNode is of type Empty</exception>
        /// <exception cref="ArgumentNullException">If rootNode or fullPath is null</exception>
        public static void LoadFile(string fullPath, LanguageNode rootNode)
        {
            if (string.IsNullOrEmpty(fullPath))
                throw new ArgumentNullException("fullPath");

            if (rootNode == null)
                throw new ArgumentNullException("rootNode");

            if (rootNode is EmptyLanguageNode)
                throw new ArgumentException("rootNode must not be of type Empty", "rootNode");

            // Clear out any old information in the language nodes
            // rootNode.ClearHierarchy(); // todo, decide what to do with this

            //TextReader reader = new StreamReader(fullPath);
            using (Stream stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.None))
            {
                TextReader reader = new StreamReader(stream);
                YamlLight yaml = YamlLight.Parse(reader);

                // go through all languages
                foreach (YamlLight language in yaml)
                {
                    int lcid;
                    if (!int.TryParse(language.Name, out lcid)) continue;
                    // go through all textstrings / categories for each language
                    foreach (YamlLight childNode in language)
                    {
                        // if the entry contains more than one line of text it is a sub root node and not just an entry in the root
                        if (childNode.Count > 0)
                        {
                            // try to retrieve the node from the root in case it's been instantiated before otherwise create it then expand
                            LanguageNode languageNode = (LanguageNode)(rootNode.GetChild(childNode.Name) ?? rootNode.AddChild(childNode.Name));
                                
                            foreach (YamlLight child in childNode)
                                Expand(child, languageNode, lcid);

                            if (!string.IsNullOrEmpty(childNode.Value))
                                rootNode.Set(childNode.Name, lcid, childNode.Value);
                        }
                        else
                            rootNode.Set(childNode.Name, lcid, childNode.Value);
                    }
                }

                reader.Dispose();
            }
        }

        private static void Expand(YamlLight node, LanguageNode parent, int lcid)
        {
            if(node.Count > 0)
            {
                ILanguageNode langNode = parent.GetChild(node.Name) ?? parent.AddChild(node.Name);

                if (!string.IsNullOrEmpty(node.Value))
					parent.Set(node.Name, lcid, node.Value);

                foreach (YamlLight subNode in node)
                    Expand(subNode, (LanguageNode)langNode, lcid);
            }
            else
                parent.Set(node.Name, lcid, node.Value);
        }

        #region IDisposable members
        /// <summary>
        /// Function to stop the watcher
        /// </summary>
        public void Dispose()
        {
            if (_watcher != null)
            {
                _watcher.Dispose();
                _watcher = null;
            }
        }
        #endregion
    }
}